﻿using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace NullTodoList
{
    /// <summary>
    /// Список для кастмоной отрисовки.
    /// </summary>
    public class CustomDrawList : ScrollableControl
    {
        // Коллекция объектов строк.
        private DrawListItems items;
        // Индекс активной строки.
        private Int32 selectedIndex;
        // Высота.
        private Int32 itemHeight;
        // Стетчик вызовов BeginUpdate()
        private Int32 beginUpdateCoutner;

        #region ctor
        /// <summary>
        /// Конструктор по-умолчанию.
        /// </summary>
        public CustomDrawList()
        {
            SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.Selectable |
                ControlStyles.Opaque | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
            
            // Коллекция обектов для строк.
            items = new DrawListItems();
            // Добавляем обработчик для изменения кол-ва строк в коллекци.
            items.CountChanged += (sender, e) => 
                {
                    CalculateScrollSize();
                    Invalidate();

                };
            // По умолчанию выбранных строк нет.
            selectedIndex = -1;
            // Высота строки по-умолчанию.
            itemHeight = 13;
            // Разрешаем автоматическое появление вертикального скроллбара.
            AutoScroll = true;
            HScroll = false;
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Расчет параметров вертикального скролла.
        /// </summary>
        private void CalculateScrollSize()
        {
            AutoScrollMinSize = new Size(0, items.Count * itemHeight);
        }

        /// <summary>
        /// Является ли индекс допустимым.
        /// </summary>
        /// <param name="index">Тестируемый индекс.</param>
        /// <returns></returns>
        private Boolean InRange(Int32 index)
        {
            return index >= 0 && index < items.Count;
        }
        #endregion

        #region Protected methods
        /// <summary>
        /// Вызов события при изменении активной строки.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnSelectedIndexChanged(EventArgs e)
        {
            if (SelectedIndexChanged != null)
                SelectedIndexChanged(this, e);
        }

        /// <summary>
        /// Отрисовка строки.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnDrawItem(DrawItemEventArgs e)
        {
            if (DrawItem != null)
                DrawItem(this, e);
        }

        /// <summary>
        /// Клавиатурные сообщения.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            switch (e.KeyCode)
            {
                case Keys.Up:
                    if (selectedIndex > 0)
                        SelectedIndex--;
                    break;
                case Keys.Down:
                    SelectedIndex++;
                    break;
            }
        }

        /// <summary>
        /// Разрешаем обрабатывать все кнопки.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e)
        {
            base.OnPreviewKeyDown(e);
            e.IsInputKey = true;
        }

        /// <summary>
        /// Клик мышкой по контролу.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            if (e.Button == MouseButtons.Left)
            {
                // Получаем индекс для текущей точки.
                var index = IndexFromPoint(e.Location);
                // Если индекс является допустимым, выделяем строку.
                if (InRange(index))
                    SelectedIndex = index;
                // Установка фокуса.
                Focus();
            }
        }

        /// <summary>
        /// Отрисовка контрола.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPaint(PaintEventArgs e)
        {
            if (beginUpdateCoutner != 0)
                return;

            base.OnPaint(e);
            // Координаты клиентской части.
            var rect = ClientRectangle;
            // Заливка фона.
            using (var brush = new SolidBrush(BackColor))
                e.Graphics.FillRectangle(brush, rect);
            // Отступ по оси Y для первой строки.
            var firstIndexOffsetY = AutoScrollPosition.Y % itemHeight;
            // Первый видимый индекс.
            var firstVisibleIndex = Math.Abs(AutoScrollPosition.Y) / itemHeight;
            // Количество видимых строк.
            var visibleItemCount = 0;
            // Высота.
            var height = Height;
            // Первая строка видна частично. Увеличиваем последний видимый индекс.
            if (firstIndexOffsetY != 0)
            {
                // Учитываем первую строку.
                visibleItemCount++;
                // Пересчитываем высоту без учета видимой высоты первой строки.
                height -= (itemHeight + firstIndexOffsetY);
            }
            // Прибавляем количество полностью видимых строк.
            visibleItemCount += (height / itemHeight);
            // Если видна часть последней строки, учитываем и ее.
            if (height % itemHeight != 0)
                visibleItemCount++;
            // Координаты первой строки.
            rect.Y = firstIndexOffsetY;
            rect.Height = itemHeight;
            // Индекс последней видимой строки.
            var lastVisibleItem = Math.Min(firstVisibleIndex + visibleItemCount, items.Count) - 1;
            // Отрисовываем строки.
            for (var i = firstVisibleIndex; i <= lastVisibleItem; i++)
            {
                // Рисуем текущую строку.
                OnDrawItem(new DrawItemEventArgs(e.Graphics, Font, rect, i, i == selectedIndex ? DrawItemState.Selected : DrawItemState.None));
                // Сдвигаем координаты.
                rect.Y += itemHeight;
            }
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Получение индекса строки по точке.
        /// </summary>
        /// <param name="point">Тестируемая точка.</param>
        /// <returns></returns>
        public Int32 IndexFromPoint(Point point)
        {
            var result = (point.Y - AutoScrollPosition.Y) / itemHeight;
            return point.X >= 0 && point.X < Width && InRange(result) ? result : -1;
        }

        /// <summary>
        /// Начало обновления.
        /// </summary>
        public void BeginUpdate()
        {
            beginUpdateCoutner++;
        }

        /// <summary>
        /// Конец обновления.
        /// </summary>
        public void EndUpdate()
        {
            if (beginUpdateCoutner == 0)
                return;
            beginUpdateCoutner--;
            if (beginUpdateCoutner == 0) this.Refresh();
                Invalidate();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Список хранимых объектов.
        /// </summary>
        [Browsable(false)]
        public DrawListItems Items
        {
            get { return items; }
            set { items = value; }
        }

        /// <summary>
        /// Индекс активной строки.
        /// </summary>
        public Int32 SelectedIndex
        {
            get { return selectedIndex; }
            set
            {
                if (selectedIndex != value)
                {
                    if (InRange(value) || value == -1)
                    {
                        selectedIndex = value;
                        OnSelectedIndexChanged(EventArgs.Empty);
                        Invalidate();
                    }
                }
            }
        }

        /// <summary>
        /// Высота строки.
        /// </summary>
        public Int32 ItemHeight
        {
            get { return itemHeight; }
            set
            {
                if (itemHeight != value)
                {
                    itemHeight = value;
                    CalculateScrollSize();
                    Invalidate();
                }
            }
        }
        #endregion

        #region Events
        /// <summary>
        /// Событие изменения индекса активной строки.
        /// </summary>
        public event EventHandler SelectedIndexChanged;
        /// <summary>
        /// Событие отрисовки строки.
        /// </summary>
        public event EventHandler<DrawItemEventArgs> DrawItem;
        #endregion
    }

    /// <summary>
    /// Список объектов, уведомляющий об изменении количества элементов.
    /// </summary>
    public class DrawListItems : ArrayList
    {
        /// <summary>
        /// Вызов события-уведомления об изменении количества эленентов.
        /// </summary>
        /// <param name="e">Пустой параметр EventArgs.</param>
        protected virtual void OnCountChanged(EventArgs e)
        {
            if (CountChanged != null)
                CountChanged(this, e);
        }

        /// <summary>
        /// Добавление объекта.
        /// </summary>
        /// <param name="value">Добавляемый объект.</param>
        /// <returns>Индекс добавленного объекта.</returns>
        public override int Add(object value)
        {
            var result = base.Add(value);
            OnCountChanged(EventArgs.Empty);
            return result;
        }

        /// <summary>
        /// Добавление коллекции объектов.
        /// </summary>
        /// <param name="c">Коллекция.</param>
        public override void AddRange(ICollection c)
        {
            base.AddRange(c);
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Очистка списка.
        /// </summary>
        public override void Clear()
        {
            base.Clear();
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Вставка объекта в определенную позицию.
        /// </summary>
        /// <param name="index">Позиция вставки.</param>
        /// <param name="value">Объект.</param>
        public override void Insert(int index, object value)
        {
            base.Insert(index, value);
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Вставка коллекции в определенную позицию.
        /// </summary>
        /// <param name="index">Позиция вставки.</param>
        /// <param name="c">Коллекция.</param>
        public override void InsertRange(int index, ICollection c)
        {
            base.InsertRange(index, c);
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Удаление объекта.
        /// </summary>
        /// <param name="obj">Удаляемый объект.</param>
        public override void Remove(object obj)
        {
            base.Remove(obj);
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Удаление объекта по индексу.
        /// </summary>
        /// <param name="index">Индекс объекта.</param>
        public override void RemoveAt(int index)
        {
            base.RemoveAt(index);
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Удаление несколько объектов с определенной позиции.
        /// </summary>
        /// <param name="index">Позиция удаления.</param>
        /// <param name="count">Количество объектов.</param>
        public override void RemoveRange(int index, int count)
        {
            base.RemoveRange(index, count);
            OnCountChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Событие изменения количества объектов.
        /// </summary>
        public event EventHandler CountChanged;
    }
}
